Citation Request:
  This dataset is public available for research. The details are described in [Cortez et al., 2009]. 
  Please include this citation if you plan to use this database:

  P. Cortez, A. Cerdeira, F. Almeida, T. Matos and J. Reis. 
  Modeling wine preferences by data mining from physicochemical properties.
  In Decision Support Systems, Elsevier, 47(4):547-553. ISSN: 0167-9236.

  Available at: [@Elsevier] http://dx.doi.org/10.1016/j.dss.2009.05.016
                [Pre-press (pdf)] http://www3.dsi.uminho.pt/pcortez/winequality09.pdf
                [bib] http://www3.dsi.uminho.pt/pcortez/dss09.bib

1. Title: Wine Quality 

2. Sources
   Created by: Paulo Cortez (Univ. Minho), Antonio Cerdeira, Fernando Almeida, Telmo Matos and Jose Reis (CVRVV) @ 2009

3. Past Usage:

  P. Cortez, A. Cerdeira, F. Almeida, T. Matos and J. Reis. 
  Modeling wine preferences by data mining from physicochemical properties.
  In Decision Support Systems, Elsevier, 47(4):547-553. ISSN: 0167-9236.

  In the above reference, two datasets were created, using red and white wine samples.
  The inputs include objective tests (e.g. PH values) and the output is based on sensory data
  (median of at least 3 evaluations made by wine experts). Each expert graded the wine quality 
  between 0 (very bad) and 10 (very excellent). Several data mining methods were applied to model
  these datasets under a regression approach. The support vector machine model achieved the
  best results. Several metrics were computed: MAD, confusion matrix for a fixed error tolerance (T),
  etc. Also, we plot the relative importances of the input variables (as measured by a sensitivity
  analysis procedure).

4. Relevant Information:

   The two datasets are related to red and white variants of the Portuguese "Vinho Verde" wine.
   For more details, consult: http://www.vinhoverde.pt/en/ or the reference [Cortez et al., 2009].
   Due to privacy and logistic issues, only physicochemical (inputs) and sensory (the output) variables 
   are available (e.g. there is no data about grape types, wine brand, wine selling price, etc.).

   These datasets can be viewed as classification or regression tasks.
   The classes are ordered and not balanced (e.g. there are munch more normal wines than
   excellent or poor ones). Outlier detection algorithms could be used to detect the few excellent
   or poor wines. Also, we are not sure if all input variables are relevant. So
   it could be interesting to test feature selection methods. 

5. Number of Instances: red wine - 1599; white wine - 4898. 

6. Number of Attributes: 11 + output attribute

   Note: several of the attributes may be correlated, thus it makes sense to apply some sort of
   feature selection.

7. Attribute information:

   For more information, read [Cortez et al., 2009].

   Input variables (based on physicochemical tests):
   1 - fixed acidity
   2 - volatile acidity
   3 - citric acid
   4 - residual sugar
   5 - chlorides
   6 - free sulfur dioxide
   7 - total sulfur dioxide
   8 - density
   9 - pH
   10 - sulphates
   11 - alcohol
   Output variable (based on sensory data): 
   12 - quality (score between 0 and 10)

8. Missing Attribute Values: None

Además de las 12 variables descritas, el dataset que utilizarás tiene otra: si el vino es blanco o rojo. Dicho esto, los objetivos son:

  1. Hacer un EDA del dataset y aplicar transformaciones si fuera necesario
  2. Separar el dataset en training (+ validación si no se hace validación cruzada) y testing, haciendo antes (o después) las transformaciones de los datos que se consideren oportunas, así como selección de variables, reducción de dimensionalidad...
  3. Hacer un modelo capaz de clasificar lo mejor posible si un vino es blanco o rojo a partir del resto de variables.
  4. Hacer un modelo regresor que prediga lo mejor posible la calidad de los vinos.
In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import plotly.express as px
from sklearn.decomposition import PCA
from sklearn.discriminant_analysis import StandardScaler
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.linear_model import Lasso
from sklearn.preprocessing import PolynomialFeatures
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_absolute_error as MAE
from sklearn.metrics import r2_score as r2
from sklearn.metrics import mean_squared_error as MSE
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
from scipy import stats
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
plt.style.use("ggplot")
warnings.filterwarnings("ignore")
LogisticRegression
Out[1]:
sklearn.linear_model._logistic.LogisticRegression
In [4]:
#importamos la tabla y hacemos una copia para trabajar
wine=pd.read_csv("C:/Users/mrold/Desktop/Master Data Science/M2. Machine Learning Python/Prácticas/winequality.csv", sep=";")
df = wine.copy()

Análisis Exploratorio de los datos¶


Comenzaremos con un análisis exploratorio de datos (EDA) básico de nuestro conjunto de datos de vinos. Aplicaremos .head() para visualizar las primeras muestras de la tabla y entender la magnitud de las variables y su naturaleza (numéricas, categóricas, ordinales, etc.).

Luego, utilizaremos .shape, .info y .describe para obtener información adicional:

.shape nos mostrará el número de filas y columnas en el conjunto de datos.
.info nos proporcionará detalles sobre los tipos de datos y la presencia de valores nulos.
.describe nos dará estadísticas básicas (como media, desviación estándar, etc.) para las variables numéricas.
Finalmente, mostraremos un conteo de los diferentes valores de la columna ‘color’, que será nuestra variable objetivo (‘target’) en nuestro primer modelo de predicción.

In [5]:
df.head(5)
Out[5]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality color
0 5.20 0.34 0.00 1.8 0.050 27.0 63.0 0.99160 3.68 0.79 14.0 6 red
1 6.20 0.55 0.45 12.0 0.049 27.0 186.0 0.99740 3.17 0.50 9.3 6 white
2 7.15 0.17 0.24 9.6 0.119 56.0 178.0 0.99578 3.15 0.44 10.2 6 white
3 6.70 0.64 0.23 2.1 0.080 11.0 119.0 0.99538 3.36 0.70 10.9 5 red
4 7.60 0.23 0.34 1.6 0.043 24.0 129.0 0.99305 3.12 0.70 10.4 5 white
In [6]:
df.shape
Out[6]:
(6497, 13)
In [7]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6497 entries, 0 to 6496
Data columns (total 13 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   fixed acidity         6497 non-null   float64
 1   volatile acidity      6497 non-null   float64
 2   citric acid           6497 non-null   float64
 3   residual sugar        6497 non-null   float64
 4   chlorides             6497 non-null   float64
 5   free sulfur dioxide   6497 non-null   float64
 6   total sulfur dioxide  6497 non-null   float64
 7   density               6497 non-null   float64
 8   pH                    6497 non-null   float64
 9   sulphates             6497 non-null   float64
 10  alcohol               6497 non-null   float64
 11  quality               6497 non-null   int64  
 12  color                 6497 non-null   object 
dtypes: float64(11), int64(1), object(1)
memory usage: 660.0+ KB

Antes de analizar las estadísticas, procederemos a eliminar posibles duplicados y valores nulos en el conjunto de datos.
Además, restableceremos el índice para evitar problemas futuros.

In [8]:
df.drop_duplicates(keep='first',inplace=True)
df.dropna()
df.reset_index(drop=True,inplace=True)
In [9]:
df.describe()
Out[9]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
count 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000 5320.000000
mean 7.215179 0.344130 0.318494 5.048477 0.056690 30.036654 114.109023 0.994535 3.224664 0.533357 10.549241 5.795677
std 1.319671 0.168248 0.147157 4.500180 0.036863 17.805045 56.774223 0.002966 0.160379 0.149743 1.185933 0.879772
min 3.800000 0.080000 0.000000 0.600000 0.009000 1.000000 6.000000 0.987110 2.720000 0.220000 8.000000 3.000000
25% 6.400000 0.230000 0.240000 1.800000 0.038000 16.000000 74.000000 0.992200 3.110000 0.430000 9.500000 5.000000
50% 7.000000 0.300000 0.310000 2.700000 0.047000 28.000000 116.000000 0.994650 3.210000 0.510000 10.400000 6.000000
75% 7.700000 0.410000 0.400000 7.500000 0.066000 41.000000 153.250000 0.996770 3.330000 0.600000 11.400000 6.000000
max 15.900000 1.580000 1.660000 65.800000 0.611000 289.000000 440.000000 1.038980 4.010000 2.000000 14.900000 9.000000
In [10]:
df['color'].value_counts()
Out[10]:
color
white    3961
red      1359
Name: count, dtype: int64

Como se puede observar, tenemos 13 columnas, de las cuales 11 son de tipo float64, 1 es de tipo int64 y 1 es de tipo object.

En nuestro primer problema de clasificación, montaremos un modelo en el cual la variable objetivo (y) será la columna ‘color’. Dicha columna se presenta en formato ‘string’, por lo que será recomendable formatearla a valores numéricos (0 y 1), ya que algunos modelos no aceptan cadenas de texto.

También se observa que los datos de tipo de color están desbalanceados: hay muchos más vinos de color ‘white’ que de color ‘red’. Esto puede ser un problema dependiendo del tipo de modelo de clasificación que utilicemos.

En este caso, podríamos considerar balancear los datos o buscar un modelo que acepte datos desbalanceados, como los ensambles.


A continuación, graficaremos todos los histogramas para ver cómo están distribuidas las características.

In [11]:
df.hist(bins=50, figsize=(20,15))
plt.show()

Se puede observar que tenemos una gran cantidad de distribuciones leptocúrticas con asimetría positiva. Esto nos indica la presencia de largas colas a la derecha en las gráficas, lo que sugiere la existencia de posibles valores atípicos (outliers). Más adelante, veremos cómo abordar este aspecto, ya que la presencia de outliers puede ocasionar grandes desventajas en algunos modelos, especialmente aquellos que son sensibles a su influencia.


Vamos a transformar la variable ‘color’ para que sea más fácil trabajar con ella en el montaje de modelos y en el análisis de correlaciones.

Codificación de ‘color’:

Convertiremos los valores de ‘color’ en números: asignaremos 1 para ‘white’ y 0 para ‘red’. Esto nos permitirá utilizar esta variable como una característica numérica en nuestros modelos.

In [12]:
tipo_vino = {'red': 0, 'white': 1}
df['color_n'] = [tipo_vino[color] for color in df['color']]
df.drop('color',axis=1,inplace=True)
In [13]:
corr = df.corr(numeric_only=True)
px_corr_mat = px.imshow(corr, width=1200,height=600, zmax=1.0, zmin=-1.0, text_auto=".2f", labels={"color": "Correlation coefficient"}, 
                        color_continuous_scale="plasma")
px_corr_mat.update_coloraxes(colorbar=dict(tickformat=".2f"))

En la tabla de correlaciones, observamos que la mayor correlación lineal para el color sería entre total sulfur dioxide y free sulfur dioxide. A continuación, lo visualizaremos en un scatterplot para los vinos de color blanco y rojo.

In [14]:
px.scatter(df, x="total sulfur dioxide", y="free sulfur dioxide",animation_frame="color_n")
In [15]:
fig = px.scatter(df, x='total sulfur dioxide', y='color_n', color='color_n',
                 color_continuous_scale='sunsetdark_r', title='Relación entre Total Sulfur Dioxide y Color',
                 labels={'total sulfur dioxide': 'Total Sulfur Dioxide', 'color_n': 'Color'})
fig.update_layout(template='none')
fig.show()

Observando este gráfico, podemos determinar que los vinos blancos presentan mayores concentraciones de dióxido de azufre en su composición que los vinos rojos.


Modelos de Clasificación para 'color'¶


Vamos a determinar las variables independiente (X) y dependiente (y) para separar el conjunto de datos en entrenamiento (train) y prueba (test):

X: Representa todo el conjunto de datos excepto la variable a predecir. En este caso, excluiremos la columna de color.
y: Representa únicamente la variable color, es decir, la variable que queremos predecir.

In [16]:
X = df.drop('color_n',axis=1) 
y = df['color_n']
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size= 0.2, random_state= 42)

Una vez que hemos dividido el conjunto de datos en las partes correspondientes de entrenamiento y prueba, procederemos a entrenar los modelos de clasificación y presentar las métricas.


SVC¶

El SVC (Support Vector Classification) es un tipo de algoritmo de aprendizaje supervisado que se utiliza comúnmente para problemas de clasificación. Como bien indica su nombre, el acrónimo hace referencia a “Support Vector Classification”. Su objetivo es encontrar un hiperplano óptimo que pueda separar de manera efectiva las clases en el espacio de características.

In [17]:
from sklearn.svm import SVC
svc=SVC()
svc.fit(X_train,y_train)
y_svc = svc.predict(X_test)
print(confusion_matrix(y_test, y_svc))
print(classification_report(y_test, y_svc))
print('Accuracy:', accuracy_score(y_test, y_svc))
[[234  50]
 [ 21 759]]
              precision    recall  f1-score   support

           0       0.92      0.82      0.87       284
           1       0.94      0.97      0.96       780

    accuracy                           0.93      1064
   macro avg       0.93      0.90      0.91      1064
weighted avg       0.93      0.93      0.93      1064

Accuracy: 0.9332706766917294

K-Nearest-neighbours¶

El algoritmo de los "k vecinos más cercanos ", también conocido como KNN, es un método de aprendizaje supervisado no paramétrico que se utiliza comúnmente para resolver problemas de clasificación y regresión. Su funcionamiento se basa en el concepto de similitud entre puntos de datos.

In [18]:
from sklearn.neighbors import KNeighborsClassifier
knc = KNeighborsClassifier()
knc.fit(X_train,y_train)
y_knc = knc.predict(X_test)
print(confusion_matrix(y_test, y_knc))
print(classification_report(y_test, y_knc))
print('Accuracy:', accuracy_score(y_test, y_knc))
[[244  40]
 [ 27 753]]
              precision    recall  f1-score   support

           0       0.90      0.86      0.88       284
           1       0.95      0.97      0.96       780

    accuracy                           0.94      1064
   macro avg       0.92      0.91      0.92      1064
weighted avg       0.94      0.94      0.94      1064

Accuracy: 0.9370300751879699

Naive Bayes¶

El Gaussian Naive Bayes (GaussianNB) es una variante del algoritmo de Naive Bayes que sigue una distribución normal gaussiana y es adecuado para datos continuos.

In [19]:
from sklearn.naive_bayes import GaussianNB
gb = GaussianNB()
gb.fit(X_train,y_train)
y_gb = gb.predict(X_test)
print(confusion_matrix(y_test, y_gb))
print(classification_report(y_test, y_gb))
print('Accuracy:', accuracy_score(y_test, y_gb))
[[276   8]
 [ 24 756]]
              precision    recall  f1-score   support

           0       0.92      0.97      0.95       284
           1       0.99      0.97      0.98       780

    accuracy                           0.97      1064
   macro avg       0.95      0.97      0.96      1064
weighted avg       0.97      0.97      0.97      1064

Accuracy: 0.9699248120300752

Nuestro conjunto de datos tiene muchos más vinos blancos que rojos. Esto crea un desequilibrio de clases. Los modelos de clasificación, como el SVM o el Naive Bayes, pueden aprender más de los vinos blancos debido a su mayor presencia en los datos. En otras palabras, podrían estar sesgados hacia la clase con mayor peso.

Posible solución: Ensambles

Para abordar este problema, podemos recurrir a los ensambles. Los ensambles combinan varios modelos para obtener una predicción más robusta. A diferencia de los modelos individuales, los ensambles, como el Random Forest o el Gradient Boosting, pueden manejar datos desbalanceados de manera más efectiva. Aceptan la realidad de que no todas las clases están representadas por igual.

Conclusión
Aunque las métricas iniciales pueden parecer prometedoras, debemos ser cautelosos al interpretar los resultados en un conjunto de datos desbalanceado.

Los ensambles nos brindan más confianza al considerar la distribución real de las clases.


Clasificación con Ensambles¶


Random Forest Classifier¶

El Random Forest Classifier (Clasificador de Bosque Aleatorio) es un algoritmo de aprendizaje supervisado que combina la salida de varios árboles de decisión para llegar a un único resultado.

In [20]:
from sklearn.ensemble import RandomForestClassifier

rfc = RandomForestClassifier(max_depth=5)
rfc.fit(X_train, y_train)
y_rfc = rfc.predict(X_test)
print(confusion_matrix(y_test, y_rfc))
print(classification_report(y_test, y_rfc))
print('Accuracy:', accuracy_score(y_test, y_rfc))
[[278   6]
 [  0 780]]
              precision    recall  f1-score   support

           0       1.00      0.98      0.99       284
           1       0.99      1.00      1.00       780

    accuracy                           0.99      1064
   macro avg       1.00      0.99      0.99      1064
weighted avg       0.99      0.99      0.99      1064

Accuracy: 0.9943609022556391

GradientBoostClassifier¶

El Gradient Boosting Classifier es un algoritmo de aprendizaje supervisado que construye un modelo aditivo en etapas, de manera progresiva.

In [21]:
from sklearn.ensemble import GradientBoostingClassifier
gbc = GradientBoostingClassifier()
gbc.fit(X_train, y_train)
y_gbc = gbc.predict(X_test)
print(confusion_matrix(y_test, y_gbc))
print(classification_report(y_test, y_gbc))
print('Accuracy:', accuracy_score(y_test, y_gbc))
[[280   4]
 [  1 779]]
              precision    recall  f1-score   support

           0       1.00      0.99      0.99       284
           1       0.99      1.00      1.00       780

    accuracy                           1.00      1064
   macro avg       1.00      0.99      0.99      1064
weighted avg       1.00      1.00      1.00      1064

Accuracy: 0.9953007518796992

Como podemos observar en estos modelos de clasificación, los puntajes para precisión, recall y F1-score son muy altos. A priori, podríamos pensar que esto se debe al sobreajuste, pero por otra parte, resulta evidente que se trata de un problema de clasificación sencillo.

En el mundo vinícola, es muy común el tratamiento de dióxidos de azufre en vinos blancos, ya que estos están desprovistos de antocianos y polifenoles que son antioxidantes naturales, a diferencia de los vinos tintos.

Esta carencia hace que los vinos blancos se oxiden y pierdan color.


Modelos de Regresión para 'quality'¶

Para los modelos de regresión, trabajaremos con una copia nueva del conjunto de datos. También utilizaremos una función definida para métricas, cortesía del Profesor Santiago :)

Montaremos los modelos en un pipeline que incluirá un escalador. Los modelos regresores requieren un escalador por varias razones:

Convergencia más rápida:

Al escalar las características, los algoritmos de optimización convergen más rápidamente. Esto es especialmente importante para modelos lineales y otros algoritmos que no son invariantes a la escala.

Evita dominancia de características:

Si las características tienen diferentes escalas, una característica con una varianza mucho mayor puede dominar el proceso de aprendizaje. El escalado ayuda a nivelar el campo de juego para todas las características.

Estabilidad del proceso de aprendizaje:

Sin escalado, los errores grandes en las etiquetas de destino pueden causar gradientes de error igualmente grandes. Esto puede hacer que los pesos del modelo cambien drásticamente, afectando la estabilidad del proceso de aprendizaje.

Además, agregaremos un PCA (Análisis de Componentes Principales) al pipeline. El PCA contribuirá a la reducción de dimensionalidad, eliminando características redundantes o irrelevantes. En resumen, el PCA ayudará a simplificar los datos y mejorar el rendimiento de los modelos de regresión.

In [22]:
df1=df.copy()
In [23]:
X1 = df1.drop('quality', axis=1)
y1 = df1['quality']

X1_train, X1_test, y1_train, y1_test = train_test_split(X1, y1, test_size=0.2, random_state=42)
In [24]:
##FUNCION DE METRICAS
from sklearn.metrics import mean_absolute_error as MAE
from sklearn.metrics import r2_score as r2
from sklearn.metrics import mean_squared_error as MSE

def metricas(modelo,X1=X1_test,y1=y1_test):
  salida={}
  salida["RMSE"]=np.sqrt(MSE(y_true=y1,y_pred=modelo.predict(X1)))
  salida["MAE"]=MAE(y_true=y1,y_pred=modelo.predict(X1))
  salida["r2"]=r2(y_true=y1,y_pred=modelo.predict(X1))
  return salida

Regresión lineal¶

La regresión lineal es una técnica estadística ampliamente utilizada que nos permite modelar y analizar la relación entre una variable dependiente y una o más variables independientes.

In [25]:
pipelr = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",LinearRegression())])
pipelr.fit(X1_train,y1_train)
metricas(pipelr)
Out[25]:
{'RMSE': 0.7741955537670795,
 'MAE': 0.5950361336635605,
 'r2': 0.2787038632701615}

Regresión Polinomica¶

La regresión polinomial es un modelo de análisis de regresión en el que la relación entre la variable independiente y la variable dependiente se modela con un polinomio de grado n.

In [26]:
polypipe = Pipeline(steps=[("escalador",StandardScaler()),('poly',PolynomialFeatures(degree=2)),("predictor",LinearRegression())])
polypipe.fit(X1_train,y1_train)
metricas(polypipe)
Out[26]:
{'RMSE': 0.7758735658434748,
 'MAE': 0.5721041720388527,
 'r2': 0.2755737618695202}

Regresión Lasso¶

Lasso (Least Absolute Shrinkage and Selection Operator) es un método de análisis de regresión que realiza selección de variables y regularización para mejorar la exactitud e interpretabilidad del modelo estadístico producido por este.

In [27]:
lasso = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",Lasso())])
lasso.fit(X1_train,y1_train)
metricas(lasso)
Out[27]:
{'RMSE': 0.9133497379789652,
 'MAE': 0.7143069139012945,
 'r2': -0.0038908075850683232}

SVR¶

El SVR (Support Vector Regression) es un algoritmo de aprendizaje supervisado que se utiliza para resolver problemas de regresión.

Basado en SVM:

El SVR es una variante del Support Vector Machine (SVM) adaptada para problemas de regresión. Al igual que el SVM, busca encontrar un hiperplano óptimo que separe los datos de entrada en dos clases (en este caso, valores de regresión).

In [28]:
from sklearn.svm import SVR


svr = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",SVR())])
svr.fit(X1_train,y1_train)
metricas(svr)
Out[28]:
{'RMSE': 0.7107316777799433,
 'MAE': 0.54179984468593,
 'r2': 0.39211195029547086}

Decision Tree Regressor¶

El Decision Tree Regressor crea un modelo en forma de árbol. Cada nodo del árbol representa una decisión basada en una característica específica. Los nodos hoja contienen predicciones numéricas.

In [29]:
dt = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",DecisionTreeRegressor())])
dt.fit(X1_train,y1_train)
metricas(dt)
Out[29]:
{'RMSE': 0.9905569178592399,
 'MAE': 0.674812030075188,
 'r2': -0.180785825671836}

KNeighbors Regressor¶

El regresor de k-vecinos es un algoritmo de aprendizaje supervisado no paramétrico.

El KN regresor predice el valor objetivo de un punto de datos basándose en la interpolación local de las etiquetas de sus vecinos más cercanos en el conjunto de entrenamiento.

In [30]:
from sklearn.neighbors import KNeighborsRegressor


knr = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",KNeighborsRegressor())])
knr.fit(X1_train,y1_train)
metricas(knr)
Out[30]:
{'RMSE': 0.7634261908476683,
 'MAE': 0.5892857142857143,
 'r2': 0.29863131588542247}

Como se observa, los modelos de regresión no están dando muy buenas métricas, aún estando procesados por un escalador y un estimador de características como el PCA.

Vamos a probar ahora diferentes ensembles regresores en nuestro pipeline.


Modelos tipo 'Ensamble'¶

Los modelos de ensamble son una técnica en aprendizaje automático que combina las predicciones de varios modelos individuales para mejorar el rendimiento general y reducir errores.

Gradient Boost¶

El boosting Añade miembros al ensamble secuencialmente para corregir las predicciones de los modelos anteriores.

In [31]:
gbr = Pipeline(steps=[("pca", PCA()),("predictor",GradientBoostingRegressor())])
gbr.fit(X1_train,y1_train)
metricas(gbr)
Out[31]:
{'RMSE': 0.7239744911524598,
 'MAE': 0.5696534950653807,
 'r2': 0.369247777962312}

Ada Boost¶

AdaBoost combina varios modelos “débiles” para crear un modelo más fuerte. A diferencia de bagging, que entrena los modelos débiles en paralelo, AdaBoost los entrena secuencialmente.

In [32]:
from sklearn.ensemble import AdaBoostRegressor


ada = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",AdaBoostRegressor())])
ada.fit(X1_train,y1_train)
metricas(ada)
Out[32]:
{'RMSE': 0.7629496367591576,
 'MAE': 0.5900580465717438,
 'r2': 0.29950667435883827}

Random Forest¶

El Random Forest Regressor usa el 'bagging'.
Lo que implica ajustar muchos árboles de decisión en diferentes muestras del mismo conjunto de datos y promediar sus predicciones. Cada árbol se entrena en una submuestra aleatoria con reemplazo del conjunto de datos original.

In [33]:
rfr = Pipeline(steps=[("escalador",StandardScaler()),("pca", PCA()),("predictor",RandomForestRegressor())])
rfr.fit(X1_train,y1_train)
metricas(rfr)
Out[33]:
{'RMSE': 0.7108991050732761,
 'MAE': 0.557077067669173,
 'r2': 0.3918255157891155}

Aquí podemos valorar varios escenarios, y es que si volvemos atrás en los histogramas, veremos claramente cómo tenemos “outliers” por tratar.

Otro escenario posible es plantear el ejercicio como un problema de clasificación en vez de un problema de regresión. Si bien la variable “quality” es numérica, no dejan de ser unas notas clasificatorias para el vino. Estas se podrían estratificar en “malo”, “bueno” y “muy bueno”, por ejemplo, aunando las calidades en cada estrato.

Además, observamos que los datos están muy desbalanceados. Se podría valorar, igual que en el ejercicio de clasificación de color, balancear dichos datos o bien pasarlos por un “ensemble” de clasificación.

Dicho esto, y siguiendo la vía del ejercicio de regresión, vamos a proceder a calcular los “z-scores” y a determinar qué “outliers” son más relevantes.


Outliers¶

A continuación, graficamos un dataset escalado para facilitar la visualización de “outliers”. Posteriormente, calcularemos los z-scores y determinaremos qué variables elegimos .

In [40]:
columnas_numericas = df.select_dtypes(include=['float64', 'int64']).columns
df_escalado = df.copy()
scaler = StandardScaler()
df_escalado[columnas_numericas] = scaler.fit_transform(df[columnas_numericas])


fig = px.box(df_escalado,title="Visionado de Outliers",orientation="h")
fig.show()
In [35]:
from scipy import stats
header = list(df1)
for feat in header:
    try:
        z = stats.zscore(df1[feat])
        print("feature: " + feat + ". Max z-score: " + str(z.max()) + ". Min z-score: " + str(z.min()))
    except TypeError:
        print("Can't calculate z-score for feature " + feat + ". It's "+ str(type(df1[feat].values[0])))
        pass      
feature: fixed acidity. Max z-score: 6.581670646808254. Min z-score: -2.588145391594692
feature: volatile acidity. Max z-score: 7.346206544940879. Min z-score: -1.5700282822148366
feature: citric acid. Max z-score: 9.116988593757808. Min z-score: -2.164515281109809
feature: residual sugar. Max z-score: 13.501066952721374. Min z-score: -0.9886038946281207
feature: chlorides. Max z-score: 15.038318117325844. Min z-score: -1.2938156177144722
feature: free sulfur dioxide. Max z-score: 14.545747651144861. Min z-score: -1.630963804070308
feature: total sulfur dioxide. Max z-score: 5.740661733347113. Min z-score: -1.9043710067929787
feature: density. Max z-score: 14.988636977558304. Min z-score: -2.504125495746094
feature: pH. Max z-score: 4.897207832811319. Min z-score: -3.1469851678941674
feature: sulphates. Max z-score: 9.795325497783612. Min z-score: -2.092830709529865
feature: alcohol. Max z-score: 3.6689829853111524. Min z-score: -2.149768162644995
feature: quality. Max z-score: 3.6425620302518196. Min z-score: -3.1780269300132162
feature: color_n. Max z-score: 0.5857432696884849. Min z-score: -1.7072325910493666

El “z-score” es una medida estadística que indica cuántas desviaciones estándar un punto de datos está del valor medio. Por eso es común usarlo como proceso estadístico para detectar los valores atípicos. Normalmente, los z-scores mayores a 3 y por debajo de -3 se consideran inusuales.

Como podemos observar en los cálculos anteriores, tenemos un gran número de “outliers” en muchas variables, siendo la más representativa la variable “Chlorides”.

In [36]:
def Zscore_outlier(df1):
    out = []
    indices_outliers = []
    
    m = np.mean(df1)
    sd = np.std(df1)
    
    for idx, i in enumerate(df1):
        z = (i - m) / sd
        if np.abs(z) > 3:
            out.append(i)
            indices_outliers.append(idx)
    
    return indices_outliers
In [37]:
fig = px.box(df1,x=df1['chlorides'],title="Antes de elimiar Outliers",orientation="h")
fig.show()

df1.drop(Zscore_outlier(df1['chlorides']),axis=0,inplace=True)
df1.reset_index(drop=True,inplace=True)

fig = px.box(df1,x=df1['chlorides'],title="Después de eliminar Outliers",orientation="h")
fig.show()
In [38]:
fig = px.box(df1,x=df1['density'],title="Antes de elimiar Outliers",orientation="h")
fig.show()
df1.drop(Zscore_outlier(df1['density']),axis=0,inplace=True)
df1.reset_index(drop=True,inplace=True)
fig = px.box(df1,x=df1['density'],title="Después de eliminar Outliers",orientation="h")
fig.show()
In [39]:
fig = px.box(df1,x=df1['residual sugar'],title="Antes de elimiar Outliers",orientation="h")
fig.show()
df1.drop(Zscore_outlier(df1['residual sugar']),axis=0,inplace=True)
df1.reset_index(drop=True,inplace=True)
fig = px.box(df1,x=df1['residual sugar'],title="Después de eliminar Outliers",orientation="h")
fig.show()
In [41]:
fig = px.box(df1,x=df1['sulphates'],title="Antes de elimiar Outliers",orientation="h")
fig.show()
df1.drop(Zscore_outlier(df1['sulphates']),axis=0,inplace=True)
df1.reset_index(drop=True,inplace=True)
fig = px.box(df1,x=df1['sulphates'],title="Después de eliminar Outliers",orientation="h")
fig.show()
In [42]:
fig = px.box(df1,x=df1['free sulfur dioxide'],title="Antes de elimiar Outliers",orientation="h")
fig.show()
df1.drop(Zscore_outlier(df1['free sulfur dioxide']),axis=0,inplace=True)
df1.reset_index(drop=True,inplace=True)
fig = px.box(df1,x=df1['free sulfur dioxide'],title="Después de eliminar Outliers",orientation="h")
fig.show()
In [43]:
X2 = df1.drop('quality', axis=1)
y2 = df1['quality']
X2_train, X2_test, y2_train, y2_test = train_test_split(X2, y2, test_size=0.2, random_state=8)
In [44]:
piperfr = Pipeline(steps=[('escalador',StandardScaler()),("pca", PCA()),("predictor",RandomForestRegressor(max_depth=100))])
piperfr.fit(X2_train,y2_train)
metricas(piperfr)
Out[44]:
{'RMSE': 0.40598564273292126,
 'MAE': 0.2825281954887218,
 'r2': 0.8016493575822514}

Vemos que la eliminación de 'outliers' ha mejorado considerablemente nuestro modelo regresor , como ultimo paso vamos a pasar el pipeline por un Grid Search 2 a 2 en hiperparametros para conseguir la mejor combinación a expectas de mejorar el rendimiento de nuestro ultimo modelo.

In [45]:
#Selección de hiperparámetros a evaluar 2x2
parametros={'escalador__with_mean':[True,False],
            "pca__n_components": [12,6,5,4],
            'pca__whiten':[True,False],
            'predictor__max_depth':[100,120,130],
            'pca__svd_solver':['auto','full'],
            'predictor__criterion':['friedman_mse','absolute error']}
In [46]:
parametros1={'escalador__with_mean':[True,False],
            "pca__n_components": [12,6,5,4]}
gs_pipe=GridSearchCV(estimator=piperfr ,param_grid=parametros1,cv=5,n_jobs=-1,verbose=3)
gs_pipe.fit(X2_train,y2_train)
gs_pipe.best_params_
Fitting 5 folds for each of 8 candidates, totalling 40 fits
Out[46]:
{'escalador__with_mean': True, 'pca__n_components': 12}
In [47]:
parametros2={'escalador__with_mean':[True],
            "pca__n_components": [12],
            'pca__whiten':[True,False],
            'predictor__max_depth':[100,120,130]}
gs_pipe1=GridSearchCV(estimator=piperfr ,param_grid=parametros2,cv=5,n_jobs=-1,verbose=3)
gs_pipe1.fit(X2_train,y2_train)
gs_pipe1.best_params_
Fitting 5 folds for each of 6 candidates, totalling 30 fits
Out[47]:
{'escalador__with_mean': True,
 'pca__n_components': 12,
 'pca__whiten': False,
 'predictor__max_depth': 130}
In [48]:
parametros3={'escalador__with_mean':[True],
            "pca__n_components": [12],
            'pca__whiten':[False],
            'predictor__max_depth':[120],
            'pca__svd_solver':['auto','full'],
            'predictor__criterion':['friedman_mse','absolute error']}
gs_pipe2=GridSearchCV(estimator=piperfr ,param_grid=parametros3,cv=5,n_jobs=-1,verbose=3)
gs_pipe2.fit(X2_train,y2_train)
gs_pipe2.best_params_
Fitting 5 folds for each of 4 candidates, totalling 20 fits
Out[48]:
{'escalador__with_mean': True,
 'pca__n_components': 12,
 'pca__svd_solver': 'full',
 'pca__whiten': False,
 'predictor__criterion': 'friedman_mse',
 'predictor__max_depth': 120}

Una vez identificados los hiperparametros óptimos, podemos hacer un último pipeline con esta configuración concreta para revisar que tanto mejoran las metricas.

In [49]:
piperfr1 = Pipeline(steps=[('escalador',StandardScaler(with_mean=True)),("pca", PCA(n_components=12,whiten=False,svd_solver='full')),("predictor",RandomForestRegressor(max_depth=120,criterion='friedman_mse'))])
piperfr1.fit(X2_train,y2_train)
metricas(piperfr1)
Out[49]:
{'RMSE': 0.4093986282855687,
 'MAE': 0.284671052631579,
 'r2': 0.7983004046801305}

Conclusión final:¶

Llegados a este punto , observando que una vez hecha una validación cruzada por gridsearchCV y una optimización de hiperparametros para un pipeline, podemos identificar que el modelo no cambia mucho del pipeline anterior, llegados a este punto podemos plantear varios escenarios para abordar:

1-Seguir tratando de procesar los Outliers y volver a entrenar los modelos para ver como se comportan.

2-Aplicar una escala logaritmica a la X , ya que esta suaviza la dispersión de valores atípicos.

3-Tratar el problema como un problema de clasificación estratificando la columna 'quality' en 3 o 2 posibilidades del tipo: 'vino malo', 'vino bueno','vino excelente' y balancenado los datos ya que el grueso en 'white' sesga el aprendizaje del modelo.

4-Tratar de reunir mas datos a nuestra fuente principal.

5-Entender de manera profunda y con conocimiento de un experto el tipo de 'features' que tenemos y como afectan estas al tipo de problema que estamos afrontando.